<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>IoT 设备网关 · 调试面板</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
  <style>
    :root {
      --bg: #0b1220;
      --panel: #121a2b;
      --panel-2: #0f172a;
      --card: #0e1627;
      --primary: #59a9ff;
      --accent: #22d3ee;
      --success: #34d399;
      --danger: #f87171;
      --text: #e5e7eb;
      --muted: #94a3b8;
      --border: #1f2937;
      --tableRow: #0d1526;
    }

    * {
      box-sizing: border-box;
    }

    html,
    body {
      margin: 0;
      height: 100%;
    }

    body {
      font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
      background: linear-gradient(180deg, var(--bg), #070b14);
      color: var(--text);
    }

    a {
      color: var(--primary);
      text-decoration: none;
    }

    .container {
      max-width: 1200px;
      margin: 0 auto;
      padding: 24px;
    }

    .header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 16px;
    }

    .title {
      font-size: 20px;
      font-weight: 600;
      letter-spacing: 0.2px;
    }

    .topbar {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 12px;
      margin-bottom: 16px;
    }

    .kpi {
      background: linear-gradient(180deg, var(--panel), var(--panel-2));
      border: 1px solid var(--border);
      border-radius: 12px;
      padding: 14px;
    }

    .kpi .label {
      font-size: 12px;
      color: var(--muted);
    }

    .kpi .value {
      font-size: 22px;
      font-weight: 700;
      margin-top: 6px;
    }

    .grid {
      display: grid;
      grid-template-columns: 1.2fr 1fr;
      gap: 16px;
    }

    .card {
      background: linear-gradient(180deg, var(--panel), var(--card));
      border: 1px solid var(--border);
      border-radius: 12px;
      padding: 16px;
    }

    .card h3 {
      margin: 0 0 10px;
      font-size: 14px;
      font-weight: 600;
      color: #cbd5e1;
    }

    .subgrid {
      display: grid;
      grid-template-columns: 1fr;
      gap: 12px;
    }

    .form-row {
      display: grid;
      grid-template-columns: 110px 1fr;
      gap: 8px;
      align-items: center;
    }

    .form-row label {
      color: var(--muted);
      font-size: 12px;
    }

    .form-row .input {
      width: 100%;
    }

    .hint {
      color: var(--muted);
      font-size: 12px;
      margin-top: 6px;
    }

    .switch {
      display: inline-flex;
      align-items: center;
      gap: 8px;
    }

    .switch input {
      width: 16px;
      height: 16px;
    }

    .toolbar {
      display: flex;
      gap: 8px;
      align-items: center;
      margin-bottom: 10px;
    }

    .input {
      background: #0b1323;
      color: var(--text);
      border: 1px solid var(--border);
      padding: 8px 10px;
      border-radius: 8px;
      outline: none;
      width: 220px;
    }

    .btn {
      background: linear-gradient(180deg, #2563eb, #1d4ed8);
      border: 1px solid #1e40af;
      color: white;
      padding: 8px 12px;
      border-radius: 8px;
      cursor: pointer;
      font-weight: 600;
    }

    .btn.secondary {
      background: linear-gradient(180deg, #0ea5e9, #0284c7);
      border-color: #0369a1;
    }

    .btn.ghost {
      background: transparent;
      border-color: var(--border);
      color: var(--text);
    }

    .btn:disabled {
      opacity: .6;
      cursor: not-allowed;
    }

    table {
      width: 100%;
      border-collapse: collapse;
    }

    th,
    td {
      padding: 10px 12px;
      text-align: left;
      border-bottom: 1px solid var(--border);
      font-size: 13px;
    }

    tbody tr:hover {
      background: var(--tableRow);
    }

    .muted {
      color: var(--muted);
      font-size: 12px;
    }

    .status-dot {
      display: inline-block;
      width: 8px;
      height: 8px;
      border-radius: 999px;
      margin-right: 6px;
    }

    .online {
      background: var(--success);
    }

    .offline {
      background: var(--danger);
    }

    .code {
      background: #0b1323;
      border: 1px solid var(--border);
      border-radius: 8px;
      padding: 10px;
      overflow: auto;
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
      font-size: 12px;
    }

    @media (max-width: 980px) {
      .grid {
        grid-template-columns: 1fr;
      }

      .topbar {
        grid-template-columns: repeat(2, 1fr);
      }
    }
  </style>
</head>

<body>
  <div id="app" class="container">
    <div class="header">
      <div class="title">IoT 设备网关 · 调试面板</div>
      <div>
        <span class="switch"><input type="checkbox" v-model="autoRefresh.enabled" /> 自动刷新</span>
        <select class="input" style="width:120px" v-model.number="autoRefresh.interval">
          <option :value="5">5s</option>
          <option :value="10">10s</option>
          <option :value="30">30s</option>
          <option :value="60">60s</option>
        </select>
        <button class="btn ghost" @click="refreshAll" :disabled="loading">立即刷新</button>
        <input class="input" style="width:220px" placeholder="API Base URL(可选，如 http://127.0.0.1:8080)"
          v-model.trim="apiBase" />
        <button class="btn ghost" @click="applyApiConfig">应用</button>
        <a class="btn ghost" href="/swagger/index.html" target="_blank">Swagger</a>
      </div>
    </div>
    <div class="grid" style="margin-top:16px;">
      <div class="card">
        <h3>推送事件 · 调试</h3>
        <div class="toolbar" style="flex-wrap:wrap;">
          <input class="input" style="width:180px" placeholder="设备ID筛选" v-model.trim="eventFilters.deviceId" />
          <input class="input" style="width:180px" placeholder="订单号筛选" v-model.trim="eventFilters.orderNo" />
          <input class="input" style="width:120px" placeholder="端口(可选)" v-model.trim="eventFilters.port" />
          <button class="btn" @click="connectSSE">SSE连接</button>
          <button class="btn" @click="startPolling">轮询</button>
          <button class="btn ghost" @click="disconnectStream">断开</button>
          <button class="btn ghost" @click="streamState.paused=!streamState.paused">{{ streamState.paused? '继续' : '暂停'
            }}</button>
          <button class="btn ghost" @click="clearEvents">清空</button>
          <button class="btn ghost" @click="copyStreamCurl">复制流 cURL</button>
          <button class="btn ghost" @click="copyRecentCurl">复制最近 cURL</button>
        </div>
        <div class="muted" style="margin-bottom:8px;">
          状态: {{ streamState.connected? '已连接' : (streamState.usePolling? '轮询中':'未连接') }}
          <span v-if="streamState.error"> · 错误: {{ streamState.error }}</span>
        </div>
        <div class="subgrid">
          <div>
            <div class="muted" style="margin-bottom:4px;">事件类型筛选</div>
            <div class="toolbar" style="flex-wrap:wrap;">
              <label class="switch"><input type="checkbox" value="device_online" v-model="eventFilters.types">
                device_online</label>
              <label class="switch"><input type="checkbox" value="device_offline" v-model="eventFilters.types">
                device_offline</label>
              <label class="switch"><input type="checkbox" value="device_register" v-model="eventFilters.types">
                device_register</label>
              <label class="switch"><input type="checkbox" value="charging_start" v-model="eventFilters.types">
                charging_start</label>
              <label class="switch"><input type="checkbox" value="charging_end" v-model="eventFilters.types">
                charging_end</label>
              <label class="switch"><input type="checkbox" value="settlement" v-model="eventFilters.types">
                settlement</label>
              <label class="switch"><input type="checkbox" value="charging_failed" v-model="eventFilters.types">
                charging_failed</label>
              <label class="switch"><input type="checkbox" value="charging_power" v-model="eventFilters.types">
                charging_power</label>
              <label class="switch"><input type="checkbox" value="power_heartbeat" v-model="eventFilters.types">
                power_heartbeat</label>
              <label class="switch"><input type="checkbox" value="port_status_change" v-model="eventFilters.types">
                port_status_change</label>
              <label class="switch"><input type="checkbox" value="port_error" v-model="eventFilters.types">
                port_error</label>
            </div>
          </div>
          <div class="toolbar">
            <span class="muted">缓冲条数</span>
            <input class="input" type="number" style="width:120px" v-model.number="eventBufferLimit" />
          </div>
        </div>
        <div class="muted" v-if="filteredEvents.length===0">暂无推送事件</div>
        <table v-else>
          <thead>
            <tr>
              <th>#</th>
              <th>时间</th>
              <th>类型</th>
              <th>设备ID</th>
              <th>端口</th>
              <th>订单号</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(ev, idx) in filteredEvents" :key="ev.event_id">
              <td>{{ idx + 1 }}</td>
              <td class="muted">{{ new Date(tsToMs(ev.timestamp)).toLocaleString() }}</td>
              <td>{{ ev.event_type }}</td>
              <td>{{ ev.device_id }}</td>
              <td class="muted">{{ ev.port_number ?? '-' }}</td>
              <td class="muted">{{ ev.data?.orderNo || '-'
                }}</td>
              <td>
                <button class="btn secondary" @click="selectedEvent=ev">详情</button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div class="card">
        <h3>事件详情与功率曲线</h3>
        <div class="toolbar">
          <select class="input" style="width:220px" v-model="selectedOrderNo" @change="updateChartSeries">
            <option :value="''">选择订单号(自动)</option>
            <option v-for="o in knownOrders" :key="o" :value="o">{{ o }}</option>
          </select>
          <span class="hint">曲线来源: charging_power / power_heartbeat</span>
        </div>
        <div id="powerChart" style="width:100%;height:260px;margin-bottom:10px;"></div>
        <div v-if="selectedEvent" class="code">
          <pre>{{ format(selectedEvent) }}</pre>
        </div>
        <div v-else class="muted">选择上方事件查看详情</div>
      </div>
    </div>

    <div class="topbar">
      <div class="kpi">
        <div class="label">系统状态</div>
        <div class="value">
          <span :class="['status-dot', health.status==='ok' ? 'online':'offline']"></span>
          {{ health.status || 'unknown' }}
        </div>
        <div class="muted">version: {{ health.version || '-' }}</div>
      </div>
      <div class="kpi">
        <div class="label">在线设备数</div>
        <div class="value">{{ statsOnline }}</div>
        <div class="muted">总数: {{ statsTotal }}</div>
      </div>
      <div class="kpi">
        <div class="label">请求耗时</div>
        <div class="value">{{ latency }} ms</div>
        <div class="muted">最近一次刷新</div>
      </div>
      <div class="kpi">
        <div class="label">错误</div>
        <div class="value" :style="{color: lastError? '#f87171':'#34d399'}">{{ lastError? '有':'无' }}</div>
        <div class="muted">{{ lastError || '正常' }}</div>
      </div>
    </div>

    <div class="grid">
      <div class="card">
        <h3>设备列表</h3>
        <div class="toolbar">
          <input class="input" placeholder="按设备ID过滤（包含匹配）" v-model.trim="filter" />
          <button class="btn" @click="refreshDevices" :disabled="loading">刷新列表</button>
        </div>
        <div class="muted" v-if="devices.length===0">暂无在线设备</div>
        <table v-else>
          <thead>
            <tr>
              <th>#</th>
              <th>设备ID</th>
              <th>连接</th>
              <th>最近心跳</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(d, idx) in filteredDevices" :key="d.deviceId">
              <td>{{ idx + 1 }}</td>
              <td>{{ d.deviceId }}</td>
              <td>
                <span class="status-dot online"></span>
                <span class="muted">online</span>
              </td>
              <td class="muted">{{ d.lastHeartbeat || '-' }}</td>
              <td>
                <button class="btn secondary" @click="selectDevice(d.deviceId)">详情</button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>

      <div class="card">
        <h3>设备详情</h3>
        <div class="toolbar">
          <input class="input" placeholder="输入设备ID，例如 04ceaa40" v-model.trim="queryId" />
          <button class="btn" @click="loadDetail(queryId)" :disabled="!queryId || loading">查询</button>
          <button class="btn secondary" @click="loadDetailQuery(queryId)"
            :disabled="!queryId || loading">查询(query)</button>
        </div>
        <div v-if="detailLoading" class="muted">加载中…</div>
        <div v-else-if="!detail">
          <div class="muted">选择列表中的设备或输入设备ID查询</div>
        </div>
        <div v-else class="code">
          <pre>{{ format(detail) }}</pre>
        </div>
        <div v-if="detailQueryResp" class="code" style="margin-top:8px;">
          <pre>{{ format(detailQueryResp) }}</pre>
        </div>
      </div>
    </div>

    <div class="grid" style="margin-top:16px;">
      <div class="card">
        <h3>操作 · 开始充电</h3>
        <div class="subgrid">
          <div class="form-row">
            <label>deviceId</label>
            <input class="input" v-model.trim="startForm.deviceId" placeholder="示例: 10644723 或 04A26CF3" />
          </div>
          <div class="form-row">
            <label>port</label>
            <input class="input" type="number" min="1" max="8" v-model.number="startForm.port" />
          </div>
          <div class="form-row">
            <label>mode</label>
            <select class="input" v-model.number="startForm.mode">
              <option :value="0">0 - 按时间(秒)</option>
              <option :value="1">1 - 按电量(0.1度)</option>
            </select>
          </div>
          <div class="form-row">
            <label>value</label>
            <input class="input" type="number" v-model.number="startForm.value" />
          </div>
          <div class="form-row">
            <label>orderNo</label>
            <input class="input" v-model.trim="startForm.orderNo" />
          </div>
          <div class="form-row">
            <label>balance</label>
            <input class="input" type="number" v-model.number="startForm.balance" placeholder="单位: 分，可选" />
          </div>
          <div class="toolbar">
            <button class="btn" @click="submitStart" :disabled="startSubmitting">发起充电</button>
            <button class="btn ghost" @click="useQueryIdFor('start')">用上方设备ID</button>
            <button class="btn ghost" @click="presetStart(1800)">预设 30 分钟</button>
            <button class="btn ghost" @click="presetStart(3600)">预设 60 分钟</button>
            <button class="btn ghost" @click="copyStartCurl">复制 cURL</button>
            <button class="btn ghost" @click="startResp=null">清空响应</button>
            <span class="hint">请求: POST /api/v1/charging/start</span>
          </div>
          <div v-if="startResp" class="code">
            <pre>{{ format(startResp) }}</pre>
          </div>
        </div>
      </div>

      <div class="card">
        <h3>操作 · 停止充电</h3>
        <div class="subgrid">
          <div class="form-row">
            <label>deviceId</label>
            <input class="input" v-model.trim="stopForm.deviceId" placeholder="示例: 10644723 或 04A26CF3" />
          </div>
          <div class="form-row">
            <label>port</label>
            <input class="input" type="number" min="1" max="255" v-model.number="stopForm.port" />
          </div>
          <div class="form-row">
            <label>orderNo</label>
            <input class="input" v-model.trim="stopForm.orderNo" placeholder="可选" />
          </div>
          <div class="toolbar">
            <button class="btn" @click="submitStop" :disabled="stopSubmitting">停止充电</button>
            <button class="btn ghost" @click="useQueryIdFor('stop')">用上方设备ID</button>
            <button class="btn ghost" @click="copyStopCurl">复制 cURL</button>
            <button class="btn ghost" @click="stopResp=null">清空响应</button>
            <span class="hint">请求: POST /api/v1/charging/stop</span>
          </div>
          <div v-if="stopResp" class="code">
            <pre>{{ format(stopResp) }}</pre>
          </div>
        </div>
      </div>
    </div>

    <div class="grid" style="margin-top:16px;">
      <div class="card">
        <h3>操作 · 调整过载功率/最大时长 (0x82重复下发)</h3>
        <div class="subgrid">
          <div class="form-row">
            <label>deviceId</label>
            <input class="input" v-model.trim="powerForm.deviceId" placeholder="示例: 10644723 或 04A26CF3" />
          </div>
          <div class="form-row">
            <label>port</label>
            <input class="input" type="number" min="1" max="8" v-model.number="powerForm.port" />
          </div>
          <div class="form-row">
            <label>orderNo</label>
            <input class="input" v-model.trim="powerForm.orderNo" placeholder="与进行中订单一致" />
          </div>
          <div class="form-row">
            <label>overloadPowerW</label>
            <input class="input" type="number" min="0" v-model.number="powerForm.overloadPowerW" />
          </div>
          <div class="form-row">
            <label>maxChargeDurationSeconds</label>
            <input class="input" type="number" min="0" v-model.number="powerForm.maxChargeDurationSeconds"
              placeholder="0=不修改" />
          </div>
          <div class="toolbar">
            <button class="btn secondary" @click="submitUpdatePower" :disabled="updatePowerSubmitting">更新功率</button>
            <button class="btn ghost" @click="useQueryIdFor('power')">用上方设备ID</button>
            <button class="btn ghost" @click="copyUpdatePowerCurl">复制 cURL</button>
            <button class="btn ghost" @click="updatePowerResp=null">清空响应</button>
            <span class="hint">请求: POST /api/v1/charging/update_power</span>
          </div>
          <div v-if="updatePowerResp" class="code">
            <pre>{{ format(updatePowerResp) }}</pre>
          </div>
        </div>
      </div>
    </div>
    <div class="grid" style="margin-top:16px;">
      <div class="card">
        <h3>操作 · 设备定位</h3>
        <div class="subgrid">
          <div class="form-row">
            <label>deviceId</label>
            <input class="input" v-model.trim="locateForm.deviceId" placeholder="示例: 10644723 或 04A26CF3" />
          </div>
          <div class="form-row">
            <label>locateTime</label>
            <input class="input" type="number" min="1" max="255" v-model.number="locateForm.locateTime" />
          </div>
          <div class="toolbar">
            <button class="btn secondary" @click="submitLocate" :disabled="locateSubmitting">定位设备</button>
            <button class="btn ghost" @click="useQueryIdFor('locate')">用上方设备ID</button>
            <button class="btn ghost" @click="copyLocateCurl">复制 cURL</button>
            <button class="btn ghost" @click="locateResp=null">清空响应</button>
            <span class="hint">请求: POST /api/v1/device/locate</span>
          </div>
          <div v-if="locateResp" class="code">
            <pre>{{ format(locateResp) }}</pre>
          </div>
        </div>
      </div>
      <div class="card">
        <h3>通知系统统计 / 限频与采样</h3>
        <div class="subgrid">
          <div class="form-row">
            <label>总发送</label>
            <div class="muted">{{ notifTotalSent ?? '-' }}</div>
          </div>
          <div class="form-row">
            <label>成功 / 失败 / 重试</label>
            <div class="muted">{{ notifTotalSuccess ?? '-' }} / {{ notifTotalFailed ?? '-' }} / {{ notifTotalRetried ??
              '-' }}</div>
          </div>
          <div class="form-row">
            <label>丢弃 · 采样</label>
            <div class="muted">{{ droppedBySampling ?? 0 }}</div>
          </div>
          <div class="form-row">
            <label>丢弃 · 节流</label>
            <div class="muted">{{ droppedByThrottle ?? 0 }}</div>
          </div>
          <div class="form-row">
            <label>平均耗时</label>
            <div class="muted">{{ notifAvgLatency ?? '-' }}</div>
          </div>
          <div class="form-row">
            <label>队列长度</label>
            <div class="muted">{{ notifQueueLen ?? '-' }} / {{ notifRetryQueueLen ?? '-' }}</div>
          </div>
          <div class="hint">注：统计字段取自 /api/v1/stats，若后端未提供对应字段将显示为 “-”。</div>
        </div>
      </div>
    </div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.global.prod.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios@1.7.8/dist/axios.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
  <script>
    const { createApp, ref, computed, onMounted, watchEffect } = Vue;

    const api = axios.create({ baseURL: '' }); // 同域（可被动态覆盖）

    createApp({
      setup() {
        const loading = ref(false);
        const devices = ref([]);
        const stats = ref({});
        const health = ref({});
        const detail = ref(null);
        const detailLoading = ref(false);
        const filter = ref('');
        const queryId = ref('');
        const lastError = ref('');
        const latency = ref(0);
        const autoRefresh = ref({ enabled: false, interval: 10, timer: null });
        const apiBase = ref('');

        // 操作表单
        const startForm = ref({
          balance: 1010,
          deviceId: '',
          mode: 0,
          orderNo: `O_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}_${Math.floor(Math.random() * 1000)}`,
          port: 1,
          value: 60,
        });
        const stopForm = ref({ deviceId: '', port: 255, orderNo: '' });
        const locateForm = ref({ deviceId: '', locateTime: 10 });
        const powerForm = ref({ deviceId: '', port: 1, orderNo: '', overloadPowerW: 0, maxChargeDurationSeconds: 0 });
        const startSubmitting = ref(false);
        const stopSubmitting = ref(false);
        const locateSubmitting = ref(false);
        const updatePowerSubmitting = ref(false);
        const startResp = ref(null);
        const stopResp = ref(null);
        const locateResp = ref(null);
        const updatePowerResp = ref(null);
        const detailQueryResp = ref(null);

        const filteredDevices = computed(() => {
          if (!filter.value) return devices.value;
          return devices.value.filter(d => String(d.deviceId).includes(filter.value));
        });
        const statsOnline = computed(() => {
          const s = stats.value || {};
          return s.onlineDeviceCount ?? (Array.isArray(s.onlineDevices) ? s.onlineDevices.length : undefined) ?? s.online ?? 0;
        });
        const statsTotal = computed(() => {
          const s = stats.value || {};
          return s.totalDeviceCount ?? s.total ?? 0;
        });

        // 通知系统统计（兼容不同命名）
        const picked = (obj, keys) => {
          for (const k of keys) {
            const parts = Array.isArray(k) ? k : [k];
            let cur = obj;
            let ok = true;
            for (const p of parts) { if (cur && p in cur) cur = cur[p]; else { ok = false; break; } }
            if (ok && cur !== undefined && cur !== null) return cur;
          }
          return undefined;
        };
        const droppedBySampling = computed(() => picked(stats.value || {}, [["notification", "dropped_by_sampling"], "dropped_by_sampling"]) ?? 0);
        const droppedByThrottle = computed(() => picked(stats.value || {}, [["notification", "dropped_by_throttle"], "dropped_by_throttle"]) ?? 0);
        const notifTotalSent = computed(() => picked(stats.value || {}, [["notification", "total_sent"], "total_sent"]))
        const notifTotalSuccess = computed(() => picked(stats.value || {}, [["notification", "total_success"], "total_success"]))
        const notifTotalFailed = computed(() => picked(stats.value || {}, [["notification", "total_failed"], "total_failed"]))
        const notifTotalRetried = computed(() => picked(stats.value || {}, [["notification", "total_retried"], "total_retried"]))
        const notifAvgLatency = computed(() => picked(stats.value || {}, [["notification", "avg_response_time"], "avg_response_time"]))
        const notifQueueLen = computed(() => picked(stats.value || {}, [["notification", "queue_length"], "queue_length"]))
        const notifRetryQueueLen = computed(() => picked(stats.value || {}, [["notification", "retry_queue_length"], "retry_queue_length"]))

        function format(obj) {
          try { return JSON.stringify(obj, null, 2); } catch { return String(obj); }
        }
        function tsToMs(ts) {
          const n = Number(ts) || 0;
          return n > 1e12 ? n : n * 1000;
        }
        function ensureSeconds(ts) {
          const n = Number(ts) || 0;
          return n > 1e12 ? Math.floor(n / 1000) : n;
        }
        function buildQuery(params) {
          return Object.entries(params)
            .filter(([k, v]) => v !== undefined && v !== null && String(v) !== '')
            .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
            .join('&');
        }

        async function fetchHealth() {
          const t0 = performance.now();
          const res = await api.get('/api/v1/health');
          health.value = res.data?.data || { status: 'ok' };
          latency.value = Math.round(performance.now() - t0);
        }

        async function fetchStats() {
          const res = await api.get('/api/v1/stats');
          stats.value = res.data?.data || {};
        }

        async function fetchDevices() {
          const res = await api.get('/api/v1/devices');
          const list = res.data?.data?.devices || [];
          // 兼容后端字段：如果没有标准字段，保持最少展示
          devices.value = list.map((it) => ({
            deviceId: it.deviceId || it.id || it.device_id || it.deviceID || it.standardId || it.standardID || it.sn || it.mac || '-'.padStart(8, '0'),
            lastHeartbeat: it.lastHeartbeat || it.last_heartbeat || it.lastSeen || it.last_seen || '',
          }));
        }

        async function loadDetail(deviceId) {
          if (!deviceId) return;
          detailLoading.value = true; lastError.value = '';
          try {
            const res = await api.get(`/api/v1/device/${encodeURIComponent(deviceId)}/status`);
            detail.value = res.data?.data || res.data;
          } catch (e) {
            lastError.value = e?.response?.data?.message || e.message || '请求失败';
            detail.value = null;
          } finally {
            detailLoading.value = false;
          }
        }

        async function loadDetailQuery(deviceId) {
          if (!deviceId) return;
          try {
            const res = await api.get(`/api/v1/device/${encodeURIComponent(deviceId)}/query`);
            detailQueryResp.value = res.data;
          } catch (e) {
            detailQueryResp.value = e?.response?.data || { error: e.message };
          }
        }

        function selectDevice(deviceId) {
          queryId.value = deviceId;
          useQueryIdFor('start');
          useQueryIdFor('stop');
          useQueryIdFor('locate');
          loadDetail(deviceId);
        }

        async function refreshDevices() {
          try { loading.value = true; lastError.value = ''; await fetchDevices(); }
          catch (e) { lastError.value = e?.response?.data?.message || e.message; }
          finally { loading.value = false; }
        }

        async function refreshAll() {
          try { loading.value = true; lastError.value = ''; await Promise.all([fetchHealth(), fetchStats(), fetchDevices()]); await refreshDetailIfSelected(); }
          catch (e) { lastError.value = e?.response?.data?.message || e.message; }
          finally { loading.value = false; }
        }

        async function refreshDetailIfSelected() {
          if (!queryId.value) return;
          try {
            const res = await api.get(`/api/v1/device/${encodeURIComponent(queryId.value)}/status`);
            detail.value = res.data?.data || res.data;
          } catch { }
        }

        // 自动刷新控制
        function setupAutoRefresh() {
          if (autoRefresh.value.timer) {
            clearInterval(autoRefresh.value.timer);
            autoRefresh.value.timer = null;
          }
          if (autoRefresh.value.enabled) {
            autoRefresh.value.timer = setInterval(() => {
              refreshAll();
            }, autoRefresh.value.interval * 1000);
          }
        }

        // 观察开关/间隔变化
        watchEffect(() => { setupAutoRefresh(); });

        // 应用 API Base URL
        function applyApiConfig() {
          const base = (apiBase.value || '').trim();
          try {
            api.defaults.baseURL = base;
          } catch { }
        }

        // 表单提交：开始充电
        async function submitStart() {
          startSubmitting.value = true; startResp.value = null; lastError.value = '';
          try {
            const payload = { ...startForm.value };
            const res = await api.post('/api/v1/charging/start', payload);
            startResp.value = res.data;
            // 成功后刷新详情与设备列表
            await refreshDetailIfSelected();
            await refreshDevices();
          } catch (e) {
            startResp.value = e?.response?.data || { error: e.message };
            lastError.value = e?.response?.data?.message || e.message;
          } finally {
            startSubmitting.value = false;
          }
        }

        // 表单提交：停止充电
        async function submitStop() {
          stopSubmitting.value = true; stopResp.value = null; lastError.value = '';
          try {
            const payload = { ...stopForm.value };
            const res = await api.post('/api/v1/charging/stop', payload);
            stopResp.value = res.data;
            // 成功后刷新详情与设备列表
            await refreshDetailIfSelected();
            await refreshDevices();
          } catch (e) {
            stopResp.value = e?.response?.data || { error: e.message };
            lastError.value = e?.response?.data?.message || e.message;
          } finally {
            stopSubmitting.value = false;
          }
        }

        // 表单提交：设备定位
        async function submitLocate() {
          locateSubmitting.value = true; locateResp.value = null; lastError.value = '';
          try {
            const payload = { ...locateForm.value };
            const res = await api.post('/api/v1/device/locate', payload);
            locateResp.value = res.data;
            // 成功后刷新详情
            await refreshDetailIfSelected();
          } catch (e) {
            locateResp.value = e?.response?.data || { error: e.message };
            lastError.value = e?.response?.data?.message || e.message;
          } finally {
            locateSubmitting.value = false;
          }
        }

        // 表单提交：调整过载功率/最大时长
        async function submitUpdatePower() {
          updatePowerSubmitting.value = true; updatePowerResp.value = null; lastError.value = '';
          try {
            const payload = { ...powerForm.value };
            const res = await api.post('/api/v1/charging/update_power', payload);
            updatePowerResp.value = res.data;
            await refreshDetailIfSelected();
          } catch (e) {
            updatePowerResp.value = e?.response?.data || { error: e.message };
            lastError.value = e?.response?.data?.message || e.message;
          } finally {
            updatePowerSubmitting.value = false;
          }
        }

        onMounted(() => { refreshAll(); setupAutoRefresh(); });

        // 便捷操作
        function useQueryIdFor(which) {
          if (!queryId.value) return;
          if (which === 'start') startForm.value.deviceId = queryId.value;
          if (which === 'stop') stopForm.value.deviceId = queryId.value;
          if (which === 'locate') locateForm.value.deviceId = queryId.value;
          if (which === 'power') powerForm.value.deviceId = queryId.value;
        }
        function presetStart(seconds) {
          startForm.value.mode = 0; // 按时间
          startForm.value.value = seconds;
        }

        // 复制 cURL 帮助
        function toJson(obj) { try { return JSON.stringify(obj); } catch { return '{}'; } }
        function b() { return (api.defaults.baseURL || '').replace(/\/$/, ''); }
        async function copy(text) { try { await navigator.clipboard.writeText(text); } catch { } }
        function copyStartCurl() {
          const payload = toJson(startForm.value);
          const url = `${b()}/api/v1/charging/start`;
          copy(`curl -X POST '${url}' -H 'Content-Type: application/json' -d '${payload}'`);
        }
        function copyStopCurl() {
          const payload = toJson(stopForm.value);
          const url = `${b()}/api/v1/charging/stop`;
          copy(`curl -X POST '${url}' -H 'Content-Type: application/json' -d '${payload}'`);
        }
        function copyLocateCurl() {
          const payload = toJson(locateForm.value);
          const url = `${b()}/api/v1/device/locate`;
          copy(`curl -X POST '${url}' -H 'Content-Type: application/json' -d '${payload}'`);
        }
        function copyUpdatePowerCurl() {
          const payload = toJson(powerForm.value);
          const url = `${b()}/api/v1/charging/update_power`;
          copy(`curl -X POST '${url}' -H 'Content-Type: application/json' -d '${payload}'`);
        }

        // ---------------- 推送事件（SSE/轮询）与功率曲线 ----------------
        const eventFilters = ref({
          types: [
            'device_online', 'device_offline', 'device_register',
            'charging_start', 'settlement', 'charging_end', 'charging_failed',
            'charging_power', 'power_heartbeat', 'port_status_change', 'port_error'
          ],
          deviceId: '',
          orderNo: '',
          port: ''
        });
        const events = ref([]); // {event_id,event_type,device_id,port_number,timestamp,data}
        const eventBufferLimit = ref(500);
        const selectedEvent = ref(null);
        const sse = ref(null);
        const streamState = ref({ connected: false, paused: false, error: '', usePolling: false, pollInterval: 10, pollTimer: null, lastSince: 0 });
        const knownOrders = computed(() => {
          const set = new Set();
          for (const ev of events.value) {
            const o = ev.data?.orderNo || ev.data?.orderId;
            if (o) set.add(String(o));
          }
          return Array.from(set);
        });
        const selectedOrderNo = ref('');
        let chart = null;
        const chartEl = ref(null);

        function normalizeEvent(payload) {
          const e = payload || {};
          const evt = {
            event_id: e.event_id || e.id || e.uuid || `${Date.now()}_${Math.random().toString(16).slice(2)}`,
            event_type: e.event_type || e.type || e.eventType || 'unknown',
            device_id: e.device_id || e.deviceId || (e.data && (e.data.device_id || e.data.deviceId)) || '',
            port_number: e.port_number ?? e.port ?? (e.data && (e.data.port_number ?? e.data.port)) ?? null,
            timestamp: e.timestamp || e.ts || e.time || Date.now(),
            data: e.data || {}
          };
          return evt;
        }

        function pushEvent(evt) {
          events.value.unshift(evt);
          if (events.value.length > eventBufferLimit.value) {
            events.value.length = eventBufferLimit.value;
          }
          // 若选择的订单未设定，自动设置
          if (!selectedOrderNo.value) {
            const o = evt.data?.orderNo || evt.data?.orderId || '';
            if (o) selectedOrderNo.value = String(o);
          }
          // 更新图表
          updateChartSeries();
        }

        const filteredEvents = computed(() => {
          const tset = new Set(eventFilters.value.types || []);
          return events.value.filter(ev => {
            if (tset.size && !tset.has(ev.event_type)) return false;
            if (eventFilters.value.deviceId && String(ev.device_id).toLowerCase().indexOf(eventFilters.value.deviceId.toLowerCase()) === -1) return false;
            const orderNo = ev.data?.orderNo || '';
            if (eventFilters.value.orderNo && String(orderNo) !== String(eventFilters.value.orderNo)) return false;
            if (eventFilters.value.port !== '' && eventFilters.value.port !== null && eventFilters.value.port !== undefined) {
              const pn = ev.port_number;
              if (String(pn) !== String(eventFilters.value.port)) return false;
            }
            return true;
          });
        });

        function connectSSE() {
          disconnectStream();
          streamState.value.error = '';
          streamState.value.usePolling = false;
          const base = (api.defaults.baseURL || '').replace(/\/$/, '');
          const params = {
            event_types: (eventFilters.value.types || []).join(','),
            device_id: (eventFilters.value.deviceId || '').trim(),
            orderNo: (eventFilters.value.orderNo || '').trim(),
            port: (eventFilters.value.port === '' || eventFilters.value.port === null || eventFilters.value.port === undefined) ? '' : String(eventFilters.value.port)
          };
          const url = `${base}/api/v1/notifications/stream?${buildQuery(params)}`;
          try {
            sse.value = new EventSource(url);
            sse.value.onopen = () => { streamState.value.connected = true; };
            sse.value.onerror = (e) => { streamState.value.error = 'SSE连接错误'; streamState.value.connected = false; };
            sse.value.onmessage = (msg) => {
              if (streamState.value.paused) return;
              try {
                const data = JSON.parse(msg.data);
                const evt = normalizeEvent(data);
                pushEvent(evt);
              } catch (err) {
                // 忽略解析错误
              }
            };
          } catch (e) {
            streamState.value.error = e?.message || 'SSE初始化失败';
          }
        }

        function startPolling() {
          disconnectStream();
          streamState.value.usePolling = true;
          const tick = async () => {
            try {
              // 获取最近事件并携带过滤参数
              const params = { limit: 100 };
              if (streamState.value.lastSince) params.since = streamState.value.lastSince; // seconds
              const types = (eventFilters.value.types || []).join(',');
              if (types) params.event_types = types;
              if ((eventFilters.value.deviceId || '').trim()) params.device_id = (eventFilters.value.deviceId || '').trim();
              if ((eventFilters.value.orderNo || '').trim()) params.orderNo = (eventFilters.value.orderNo || '').trim();
              if (eventFilters.value.port !== '' && eventFilters.value.port !== null && eventFilters.value.port !== undefined) params.port = String(eventFilters.value.port);
              const res = await api.get('/api/v1/notifications/recent', { params });
              const list = res.data?.data || res.data || [];
              for (const it of list) {
                const evt = normalizeEvent(it);
                pushEvent(evt);
                streamState.value.lastSince = Math.max(streamState.value.lastSince, ensureSeconds(evt.timestamp));
              }
            } catch { }
          };
          tick();
          streamState.value.pollTimer = setInterval(() => { if (!streamState.value.paused) tick(); }, streamState.value.pollInterval * 1000);
        }

        function disconnectStream() {
          if (sse.value) { try { sse.value.close(); } catch { } sse.value = null; }
          if (streamState.value.pollTimer) { clearInterval(streamState.value.pollTimer); streamState.value.pollTimer = null; }
          streamState.value.connected = false;
        }

        function clearEvents() { events.value = []; selectedEvent.value = null; updateChartSeries(); }

        function copyStreamCurl() {
          const base = (api.defaults.baseURL || '').replace(/\/$/, '');
          const params = {
            event_types: (eventFilters.value.types || []).join(','),
            device_id: (eventFilters.value.deviceId || '').trim(),
            orderNo: (eventFilters.value.orderNo || '').trim(),
            port: (eventFilters.value.port === '' || eventFilters.value.port === null || eventFilters.value.port === undefined) ? '' : String(eventFilters.value.port)
          };
          const url = `${base}/api/v1/notifications/stream?${buildQuery(params)}`;
          const cmd = `curl '${url}'`;
          navigator.clipboard.writeText(cmd).catch(() => { });
        }

        function copyRecentCurl() {
          const base = (api.defaults.baseURL || '').replace(/\/$/, '');
          const params = { limit: 100 };
          if (streamState.value.lastSince) params.since = streamState.value.lastSince;
          const types = (eventFilters.value.types || []).join(',');
          if (types) params.event_types = types;
          if ((eventFilters.value.deviceId || '').trim()) params.device_id = (eventFilters.value.deviceId || '').trim();
          if ((eventFilters.value.orderNo || '').trim()) params.orderNo = (eventFilters.value.orderNo || '').trim();
          if (eventFilters.value.port !== '' && eventFilters.value.port !== null && eventFilters.value.port !== undefined) params.port = String(eventFilters.value.port);
          const url = `${base}/api/v1/notifications/recent?${buildQuery(params)}`;
          const cmd = `curl '${url}'`;
          navigator.clipboard.writeText(cmd).catch(() => { });
        }

        function toPowerValue(ev) {
          const d = ev?.data || {};
          const v = d.power ?? d.p ?? null;
          if (v == null) return null;
          return Number(v);
        }

        function updateChartSeries() {
          if (!chart) return;
          const seriesData = {};
          for (const ev of events.value) {
            if (!(ev.event_type === 'charging_power' || ev.event_type === 'power_heartbeat')) continue;
            const orderNo = ev.data?.orderNo || '';
            if (!orderNo) continue;
            const pw = toPowerValue(ev);
            if (pw == null) continue;
            const ts = Number(new Date(ev.timestamp)) || Date.now();
            // use backend seconds -> ms conversion
            const tsm = tsToMs(ev.timestamp);
            if (!seriesData[orderNo]) seriesData[orderNo] = [];
            seriesData[orderNo].push([tsm, pw]);
          }
          const order = selectedOrderNo.value && seriesData[selectedOrderNo.value] ? selectedOrderNo.value : Object.keys(seriesData)[0] || '';
          if (order && !selectedOrderNo.value) selectedOrderNo.value = order;
          const data = order ? (seriesData[order] || []).slice().sort((a, b) => a[0] - b[0]) : [];
          chart.setOption({
            tooltip: { trigger: 'axis', valueFormatter: (v) => `${v} W` },
            xAxis: { type: 'time' },
            yAxis: { type: 'value', name: '功率(W)' },
            series: [{ type: 'line', name: '功率', showSymbol: false, data }]
          });
        }

        onMounted(() => {
          // 初始化图表
          const el = document.getElementById('powerChart');
          if (el && window.echarts) {
            chart = echarts.init(el);
            updateChartSeries();
            window.addEventListener('resize', () => { chart && chart.resize(); });
          }
        });

        return {
          loading, devices, stats, statsOnline, statsTotal, health, detail, detailLoading, filter, queryId, lastError, latency, autoRefresh, apiBase, devices: devices, filteredDevices, format, loadDetail, loadDetailQuery, refreshDevices, refreshAll, applyApiConfig, startForm, stopForm, locateForm, powerForm, startSubmitting, stopSubmitting, locateSubmitting, updatePowerSubmitting, startResp, stopResp, locateResp, updatePowerResp, detailQueryResp, submitStart, submitStop, submitLocate, submitUpdatePower, useQueryIdFor, presetStart, selectDevice, copyStartCurl, copyStopCurl, copyLocateCurl, copyUpdatePowerCurl,
          // events
          events, eventFilters, filteredEvents, selectedEvent, eventBufferLimit, streamState, knownOrders, selectedOrderNo,
          connectSSE, startPolling, disconnectStream, clearEvents, copyStreamCurl, copyRecentCurl, updateChartSeries, tsToMs
        };
      }
    }).mount('#app');
  </script>
</body>

</html>